package org.zoomdy.gdbx;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.ServerSocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.zoomdy.gdbx.Packet.PacketType;

/*
 * Node 负责管理连接和数据收发，Packet 负责数据解析。
 */
public class Node implements Runnable{
    final static String version = "1.1.0";
    
    enum NodeState
    {
        INIT,
        LISTEN,
        ACCEPTED,
        CONNECTED,
        CLOSED,
        QUIT,
    }
    
    enum ExtenderMode
    {
        NONE,
        SERVER,
        CLIENT,
    }
    
    final int CHANNEL_SERVER    = 0x01;
    final int CHANNEL_CLIENT    = 0x02;
    final int CHANNEL_EXTENDER  = 0x04;
    final int CHANNEL_ANY       = CHANNEL_SERVER | 
            CHANNEL_CLIENT | CHANNEL_EXTENDER;
    
    String              clientHost;
    int                 clientPort;
    SocketChannel       clientChannel;
    ByteBuffer          clientBuffer;
    
    int                 serverPort;
    ServerSocketChannel serverListenChannel;
    SocketChannel       serverChannel;
    ByteBuffer          serverBuffer;
    
    String              extenderHost;
    int                 extenderPort;
    ServerSocketChannel extenderListenChannel;
    SocketChannel       extenderChannel;
    ByteBuffer          extenderBuffer;
    
    NodeState           nodeState;
    ExtenderMode        extenderMode;
    Packet.ProtocolType extenderProtocol;
    
    boolean             runOnce;
    
    String              id;
    Logger              logger;
    
    boolean             noAckMode;
    
    int                 debugLevel;
    
    public Node(String id)
    {
        this.clientHost = "localhost";
        this.clientPort = 9000;
        this.serverPort = 9001;
        this.nodeState = NodeState.INIT;
        this.extenderMode = ExtenderMode.NONE;
        this.extenderProtocol = Packet.ProtocolType.GDB;
        this.runOnce = false;
        this.id = id;
        this.logger = Logger.getLogger("org.zoomdy.gdbx");
        this.debugLevel = 1;
    }
    
    public void setLoggerLevel(Level level)
    {
        this.logger.setLevel(level);
    }
    
    public String getId()
    {
        return id;
    }

    public void setId(String id)
    {
        this.logger.entering("org.zoomdy.gdbx.Node", "setId", id);
        
        this.id = id;
        
        this.logger.exiting("org.zoomdy.gdbx.Node", "setId");
    }
    
    public void setServerPort(int port)
    {
        this.logger.entering("org.zoomdy.gdbx.Node", "setServerPort", port);
        
        this.serverPort = port;
        
        this.logger.exiting("org.zoomdy.gdbx.Node", "setServerPort");
    }
    public int getServerPort()
    {
        return this.serverPort;
    }
    
    public void setClientHost(String host)
    {
        this.logger.entering("org.zoomdy.gdbx.Node", "setClientHost", host);
        
        this.clientHost = host;
        
        this.logger.exiting("org.zoomdy.gdbx.Node", "setClientHost");
    }
    public String getClientHost()
    {
        return this.clientHost;
    }
    
    public void setClientPort(int port)
    {
        this.logger.entering("org.zoomdy.gdbx.Node", "setClientPort", port);
        
        this.clientPort = port;
        
        this.logger.exiting("org.zoomdy.gdbx.Node", "setClientPort");
    }
    public int getClientPort()
    {
        return this.clientPort;
    }
    
    public String getExtenderHost()
    {
        return extenderHost;
    }

    public void setExtenderHost(String extenderHost)
    {
        this.logger.entering("org.zoomdy.gdbx.Node", "setExtenderHost", 
                extenderHost);
        
        this.extenderHost = extenderHost;
        this.extenderMode = ExtenderMode.CLIENT;
        
        this.logger.exiting("org.zoomdy.gdbx.Node", "setExtenderHost");
    }

    public int getExtenderPort()
    {
        return extenderPort;
    }

    public void setExtenderPort(int extenderPort)
    {
        this.logger.entering("org.zoomdy.gdbx.Node", "setExtenderPort", 
                extenderPort);
        
        this.extenderPort = extenderPort;
        if(this.extenderHost == null)
        {
            this.extenderMode = ExtenderMode.SERVER;
        }
        
        this.logger.exiting("org.zoomdy.gdbx.Node", "setExtenderPort");
    }
    
    public Packet.ProtocolType getExtenderProtocol()
    {
        return extenderProtocol;
    }

    public void setExtenderProtocol(Packet.ProtocolType extenderProtocol)
    {
        this.logger.entering("org.zoomdy.gdbx.Node", "setExtenderProtocol", 
                extenderProtocol);
        
        this.extenderProtocol = extenderProtocol;
        
        this.logger.exiting("org.zoomdy.gdbx.Node", "setExtenderProtocol");
    }
    
    public boolean isRunOnce()
    {
        return runOnce;
    }

    public void setRunOnce(boolean runOnce)
    {
        this.runOnce = runOnce;
    }

    public int getDebugLevel()
    {
        return debugLevel;
    }

    public void setDebugLevel(int debugLevel)
    {
        this.debugLevel = debugLevel;
    }
    
    public void debug(int debugLevel, Object obj)
    {
        if(this.debugLevel >= debugLevel)
        {
            System.out.println("<" + debugLevel + ">" + this.getClass().getSimpleName() + ": " + obj);
        }
    }
    
    public void debug(int debugLevel, int channels, Object obj)
    {
        if(this.debugLevel < debugLevel)
        {
            return;
        }
        
        if((channels & this.CHANNEL_CLIENT) == this.CHANNEL_CLIENT)
        {
            System.out.println("<" + debugLevel + ">" + this.getClass().getSimpleName() + ".client: " + obj);
        }
        
        if((channels & this.CHANNEL_SERVER) == this.CHANNEL_SERVER)
        {
            System.out.println("<" + debugLevel + ">" + this.getClass().getSimpleName() + ".server: " + obj);
        }
        
        if((channels & this.CHANNEL_EXTENDER) == this.CHANNEL_EXTENDER)
        {
            if(this.extenderChannel != null)
            {
                System.out.println("<" + debugLevel + ">" + this.getClass().getSimpleName() + ".extender: " + obj);
            }
        }
    }
    
    String getVersion()
    {
        return Node.version;
    }
    
    void doInit() throws IOException
    {
        this.logger.entering("org.zoomdy.gdbx.Node", "doInit");
        
        this.debug(1, this.getClass().getName() + " " + this.getVersion());
        this.debug(1, "Report bugs to mingdu.zheng@gmail.com");
        this.debug(1, "GDB Server Extender home page: http://gdbx.zoomdy.org");
        
        this.serverBuffer = ByteBuffer.allocate(Packet.BUFFER_CAPACITY);
        // 大多数情况下，buffer处于读模式，仅在接受数据的时候需要设置成写模式
        this.serverBuffer.flip();
        
        this.clientBuffer = ByteBuffer.allocate(Packet.BUFFER_CAPACITY);
        // 大多数情况下，buffer处于读模式，仅在接受数据的时候需要设置成写模式
        this.clientBuffer.flip();
        
        if(this.extenderMode != ExtenderMode.NONE)
        {
            this.extenderBuffer = ByteBuffer.allocate(Packet.BUFFER_CAPACITY);
            // 大多数情况下，buffer处于读模式，仅在接受数据的时候需要设置成写模式
            this.extenderBuffer.flip();
        }
        
        this.serverListenChannel = ServerSocketChannel.open();
        this.serverListenChannel.socket();
        this.serverListenChannel.bind(new InetSocketAddress(this.serverPort));
        this.serverListenChannel.configureBlocking(false);
        this.debug(3, this.CHANNEL_SERVER, "bind to " + this.serverPort);
        
        if(this.extenderMode == ExtenderMode.SERVER)
        {
            assert this.extenderPort > 0;
            
            this.extenderListenChannel = ServerSocketChannel.open();
            this.extenderListenChannel.socket();
            this.extenderListenChannel.bind(
                    new InetSocketAddress(this.extenderPort));
            this.extenderListenChannel.configureBlocking(false);
            this.debug(3, this.CHANNEL_EXTENDER, "bind to " + this.extenderPort);
        }
        
        this.nodeState = NodeState.LISTEN;
        this.debug(3, "Go LISTEN state.");
        
        this.logger.exiting("org.zoomdy.gdbx.Node", "doInit");
    }
    
    void doListen() throws IOException
    {
        this.logger.entering("org.zoomdy.gdbx.Node", "doListen");
        
        Selector selector = Selector.open();
        this.serverListenChannel.register(selector, SelectionKey.OP_ACCEPT);
        while(this.nodeState != NodeState.ACCEPTED)
        {
            int readyChannels = selector.select();
            if(readyChannels == 0)
            {
                continue;
            }
            
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
            while(keyIterator.hasNext())
            {
                SelectionKey key1 = keyIterator.next();
                if(key1.isAcceptable())
                {
                    this.serverChannel = this.serverListenChannel.accept();
                    this.serverChannel.configureBlocking(false);
                    this.debug(1, this.CHANNEL_SERVER, 
                            "accept " + this.serverChannel.getRemoteAddress().toString());
                    
                    this.nodeState = NodeState.ACCEPTED;
                    this.debug(3, "Go ACCEPTED state");
                }
                keyIterator.remove();
            }
        }
        
        this.logger.exiting("org.zoomdy.gdbx.Node", "doListen");
    }
    
    void doAccepted() throws IOException
    {
        this.logger.entering("org.zoomdy.gdbx.Node", "doAccepted");
        
        this.clientChannel = SocketChannel.open();
        this.clientChannel.connect(
                new InetSocketAddress(this.clientHost, this.clientPort));
        this.clientChannel.configureBlocking(false);
        this.debug(1, this.CHANNEL_CLIENT, 
                "connect to " + this.clientHost + ":" + this.clientPort);
        
        if(this.extenderMode == ExtenderMode.CLIENT)
        {
            assert this.extenderHost != null;
            assert this.extenderPort > 0;
            
            this.extenderChannel = SocketChannel.open();
            this.extenderChannel.connect(
                    new InetSocketAddress(this.extenderHost, this.extenderPort));
            this.extenderChannel.configureBlocking(false);
            this.debug(1, this.CHANNEL_EXTENDER, 
                    "connect to " + this.extenderHost + ":" + this.extenderPort);
        }
        
        this.nodeState = NodeState.CONNECTED;
        this.debug(3, "Go CONNECTED state");
        
        // 默认连接方式：AckMode
        this.noAckMode = false;
        
        this.logger.exiting("org.zoomdy.gdbx.Node", "doAccepted");
    }
    
    Packet doConnected(int channels) throws IOException
    {
        assert channels > 0;
        
        this.logger.entering("org.zoomdy.gdbx.Node", "doConnected", channels);
        
        Packet packet = new Packet();
        /*
         * ====================================================================
         */
        if((channels & this.CHANNEL_SERVER) == this.CHANNEL_SERVER && 
                this.serverBuffer.hasRemaining())
        {
            this.serverBuffer = packet.fetch(this.serverBuffer);
            if(packet.isValid())
            {
                packet.setChannels(this.CHANNEL_SERVER);
                this.debug(3, this.CHANNEL_SERVER, 
                        "fetch from buffer " + packet.buffer.limit() + "bytes");
                return packet;
            }
        }
        if((channels & this.CHANNEL_CLIENT) == this.CHANNEL_CLIENT &&
                this.clientBuffer.hasRemaining())
        {
            this.clientBuffer = packet.fetch(this.clientBuffer);
            if(packet.isValid())
            {
                packet.setChannels(this.CHANNEL_CLIENT);
                this.debug(3, this.CHANNEL_CLIENT, 
                        "fetch from buffer " + packet.buffer.limit() + "bytes");
                return packet;
            }
        }
        if((channels & this.CHANNEL_EXTENDER) == this.CHANNEL_EXTENDER && 
                this.extenderBuffer != null && this.extenderBuffer.hasRemaining())
        {
            this.extenderBuffer = packet.fetch(this.extenderBuffer, 
                    this.extenderProtocol);
            if(packet.isValid())
            {
                packet.setChannels(this.CHANNEL_EXTENDER);
                this.debug(3, this.CHANNEL_EXTENDER, 
                        "fetch from buffer " + packet.buffer.limit() + "bytes");
                return packet;
            }
        }
        
        /*
         * ====================================================================
         */
        Selector selector = Selector.open();
        
        if((channels & this.CHANNEL_SERVER) == this.CHANNEL_SERVER)
        {
            this.serverChannel.register(selector, SelectionKey.OP_READ);
        }
        
        if((channels & this.CHANNEL_CLIENT) == this.CHANNEL_CLIENT)
        {
            this.clientChannel.register(selector, SelectionKey.OP_READ);
        }
        
        if(this.extenderMode == ExtenderMode.CLIENT && 
                (channels & this.CHANNEL_EXTENDER) == this.CHANNEL_EXTENDER)
        {
            assert this.extenderChannel != null;
            
            this.extenderChannel.register(selector, SelectionKey.OP_READ);
        }
        
        if(this.extenderMode == ExtenderMode.SERVER && 
                this.extenderChannel == null &&
                (channels & this.CHANNEL_EXTENDER) == this.CHANNEL_EXTENDER)
        {
            assert this.extenderListenChannel != null;
            
            this.extenderListenChannel.register(selector, SelectionKey.OP_ACCEPT);
        }
        
        /*
         * ====================================================================
         */
        while(!packet.isValid() && this.nodeState == NodeState.CONNECTED)
        {
            int readyChannels = selector.select();
            if(readyChannels == 0)
            {
                continue;
            }
            
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
            while(keyIterator.hasNext())
            {
                SelectionKey key = keyIterator.next();
                if(key.isReadable())
                {
                    assert key.channel() != null;
                    assert key.channel() == this.serverChannel || 
                            key.channel() == this.clientChannel || 
                            key.channel() == this.extenderChannel;
                    
                    ByteBuffer buffer;
                    int currentChannel;
                    if(key.channel() == this.serverChannel)
                    {
                        buffer = this.serverBuffer;
                        currentChannel = this.CHANNEL_SERVER;
                    }
                    else if(key.channel() == this.clientChannel)
                    {
                        buffer = this.clientBuffer;
                        currentChannel = this.CHANNEL_CLIENT;
                    }
                    else if(this.extenderBuffer != null && 
                            key.channel() == this.extenderChannel)
                    {
                        buffer = this.extenderBuffer;
                        currentChannel = this.CHANNEL_EXTENDER;
                    }
                    else
                    {
                        buffer = null;
                        currentChannel = 0;
                    }
                    assert buffer != null;
                    
                    // 读数据之前，将buffer设置为写模式
                    buffer.compact();
                    int bytesRead;
                    do
                    {
                        bytesRead = ((SocketChannel) key.channel()).read(buffer);
                    }
                    while(bytesRead > 0);
                    
                    // 数据读取完成后，立即将buffer重设为读模式
                    buffer.flip();
                    
                    if(bytesRead == -1)
                    {
                        this.debug(1, currentChannel, "remote peer closed");
                        
                        this.nodeState = NodeState.CLOSED;
                        this.debug(3, "Go CLOSED state");
                        break;
                    }
                    
                    this.debug(3, currentChannel, 
                            "recved " + buffer.limit() + "bytes");
                    
                    if(key.channel() == this.serverChannel)
                    {
                        this.serverBuffer = packet.fetch(this.serverBuffer);
                        if(packet.isValid())
                        {
                            packet.setChannels(this.CHANNEL_SERVER);
                        }
                    }
                    else if(key.channel() == this.clientChannel)
                    {
                        this.clientBuffer = packet.fetch(this.clientBuffer);
                        if(packet.isValid())
                        {
                            packet.setChannels(this.CHANNEL_CLIENT);
                        }
                    }
                    else if(this.extenderBuffer != null && 
                            key.channel() == this.extenderChannel)
                    {
                        this.extenderBuffer = packet.fetch(this.extenderBuffer, 
                                this.extenderProtocol);
                        if(packet.isValid())
                        {
                            packet.setChannels(this.CHANNEL_EXTENDER);
                        }
                    }
                    else
                    {
                        assert false;
                    }
                    
                    if(packet != null && packet.isValid())
                    {
                        this.debug(3, currentChannel, 
                                "fetch from recv " + packet.buffer.limit() + "bytes");
                    }
                    
                    if(packet.isValid())
                    {
                        keyIterator.remove();
                        break;
                    }
                }
                else if(key.isAcceptable())
                {
                    assert key.channel() != null;
                    assert key.channel() == this.extenderListenChannel;
                    
                    this.extenderChannel = this.extenderListenChannel.accept();
                    this.extenderChannel.configureBlocking(false);
                    this.debug(1, this.CHANNEL_EXTENDER, 
                            "accept " + this.extenderChannel.getRemoteAddress().toString());
                }
                keyIterator.remove();
            }
        }
        selector.close();
        
        this.logger.exiting("org.zoomdy.gdbx.Node", "doConnected");
        
        if(packet.isValid())
        {
            return packet;
        }
        else
        {
            return null;
        }
    }
    
    void doClosed() throws IOException
    {
        assert this.clientChannel != null;
        assert this.serverChannel != null;
        
        this.logger.entering("org.zoomdy.gdbx.Node", "doClosed");
        
        this.clientChannel.close();
        this.clientChannel = null;
        this.clientBuffer.clear();
        this.clientBuffer.flip();
        this.debug(1, this.CHANNEL_CLIENT, "close");
        
        this.serverChannel.close();
        this.serverChannel = null;
        this.serverBuffer.clear();
        this.serverBuffer.flip();
        this.debug(1, this.CHANNEL_SERVER, "close");
        
        if(this.extenderChannel != null)
        {
            this.extenderChannel.close();
            this.extenderChannel = null;
            this.extenderBuffer.clear();
            this.extenderBuffer.flip();
            this.debug(1, this.CHANNEL_EXTENDER, "close");
        }
        
        if(this.runOnce)
        {
            this.nodeState = NodeState.QUIT;
            this.debug(3, "Go QUIT state");
        }
        else
        {
            this.nodeState = NodeState.LISTEN;
            this.debug(3, "Go LISTEN state");
        }
        
        this.logger.exiting("org.zoomdy.gdbx.Node", "doClosed");
    }

    public Packet recvPacket(int channels) throws IOException
    {
        assert channels > 0;
        
        this.logger.entering("org.zoomdy.gdbx.Node", "recvPacket", channels);
        
        Packet packet = null;
        while(packet == null)
        {
            if(this.nodeState == NodeState.INIT)
            {
                this.doInit();
            }
            else if(this.nodeState == NodeState.LISTEN)
            {
                this.doListen();
            }
            else if(this.nodeState == NodeState.ACCEPTED)
            {
                this.doAccepted();
            }
            else if(this.nodeState == NodeState.CONNECTED)
            {
                packet = this.doConnected(channels);
                if(packet != null)
                {
                    this.printPacket("recv", packet);
                }
            }
            else if(this.nodeState == NodeState.CLOSED)
            {
                this.doClosed();
            }
            else if(this.nodeState == NodeState.QUIT)
            {
                break;
            }
            else
            {
                
            }
        }
        
        if(packet != null &&  packet.channels == this.CHANNEL_SERVER && 
                packet.packetType == Packet.PacketType.HEX && 
                packet.startsWith("QStartNoAckMode"))
        {
            this.noAckMode = true;
            this.debug(1, "start no ack mode");
        }
        
        this.logger.exiting("org.zoomdy.gdbx.Node", "recvPacket");
        
        return packet;
    }
    
    public void sendPacket(Packet packet) throws IOException
    {
        assert packet != null;
        
        this.logger.entering("org.zoomdy.gdbx.Node", "sendPacket", packet);
        
        this.debug(3, packet.channels,
                "send " + packet.buffer.limit() + "bytes");
        
        if(packet.channels > 0)
        {
            this.printPacket("send", packet);
        }
        
        if((packet.channels & this.CHANNEL_SERVER) == this.CHANNEL_SERVER)
        {
            assert this.serverChannel != null;
            this.serverChannel.write(packet.buffer);
        }
        if ((packet.channels & this.CHANNEL_CLIENT) == this.CHANNEL_CLIENT)
        {
            assert this.clientChannel != null;
            this.clientChannel.write(packet.buffer);
        }
        if((packet.channels & this.CHANNEL_EXTENDER) == this.CHANNEL_EXTENDER)
        {
            if(this.extenderChannel != null)
            {
                this.extenderChannel.write(packet.buffer);
            }
        }
        
        this.logger.exiting("org.zoomdy.gdbx.Node", "sendPacket");
    }
    
    public void run()
    {
        assert this.serverPort > 0;
        assert this.clientHost != null;
        assert this.clientPort > 0;
        
        try
        {
            while(this.nodeState != NodeState.QUIT)
            {
                this.logger.entering("org.zoomdy.gdbx.Node", "run");
                
                Packet packet = this.recvPacket(this.CHANNEL_ANY);
                if(packet == null)
                {
                    continue;
                }
                
                if((packet.channels & this.CHANNEL_SERVER) == this.CHANNEL_SERVER)
                {
                    packet.channels = this.CHANNEL_CLIENT;
                    this.sendPacket(packet);
                }
                else if((packet.channels & this.CHANNEL_CLIENT) == this.CHANNEL_CLIENT)
                {
                    packet.channels = this.CHANNEL_SERVER;
                    this.sendPacket(packet);
                }
                else if((packet.channels & this.CHANNEL_EXTENDER) == this.CHANNEL_EXTENDER)
                {
                    // nothing
                }
                
                this.logger.exiting("org.zoomdy.gdbx.Node", "run");
            }
        }
        catch(IOException e) 
        {
            e.printStackTrace();
        }
    }
    
    public String[] parseArgs(String[] args)
    {
        String[] remainArgs = new String[args.length];
        int remainArgsIndex = 0;
        
        for(int i = 0; i < args.length; i++)
        {
            if(args[i].startsWith("--client-host="))
            {
                this.setClientHost(args[i].split("=")[1]);
            }
            else if(args[i].startsWith("--client-port="))
            {
                this.setClientPort(Integer.parseInt(args[i].split("=")[1]));
            }
            else if(args[i].startsWith("--server-port="))
            {
                this.setServerPort(Integer.parseInt(args[i].split("=")[1]));
            }
            else if(args[i].startsWith("--extender-host="))
            {
                this.setExtenderHost(args[i].split("=")[1]);
            }
            else if(args[i].startsWith("--extender-port="))
            {
                this.setExtenderPort(Integer.parseInt(args[i].split("=")[1]));
            }
            else if(args[i].startsWith("--extender-protocol="))
            {
                String protocol = args[i].split("=")[1];
                if(protocol.equals("line"))
                {
                    this.setExtenderProtocol(Packet.ProtocolType.LINE);
                }
                else if(protocol.equals("gdb"))
                {
                    this.setExtenderProtocol(Packet.ProtocolType.GDB);
                }
                else
                {
                    remainArgs[remainArgsIndex] = args[i];
                    remainArgsIndex++;
                }
            }
            else if(args[i].equals("--run-once"))
            {
                this.setRunOnce(true);
            }
            else if(args[i].startsWith("--debug="))
            {
                this.setDebugLevel(Integer.parseInt(args[i].split("=")[1]));
            }
            else
            {
                remainArgs[remainArgsIndex] = args[i];
                remainArgsIndex++;
            }
        }
        
        return remainArgs;
    }
    
    public String usage()
    {
        String s = "Usage:\n";
        s += "--client-host=hostname        hostname of next gdb server\n";
        s += "      default: localhost\n";
        s += "--client-port=port            port of next gdb server\n";
        s += "      default: 9000\n";
        s += "--server-port=port            port of this gdb server\n";
        s += "      default: 9001\n";
        s += "--extender-host=hostname      hostname of extender\n";
        s += "      set this option made extender as client\n";
        s += "      skip this option and set --extender-port made extender as server\n";
        s += "      no default value\n";
        s += "--extender-port=port          port of extender\n";
        s += "      if extender as client, it's the port which connect to\n";
        s += "      if extender as server, it's the port which bind to lisnten\n";
        s += "      if --extender-host was set then default: 9002, else no default\n";
        s += "--extender-protocol=gdb|line  protocol of extender\n";
        s += "      default:gdb\n";
        s += "--run-once                    quit when connect is closed\n";
        s += "--debug=debug-level\n";
        s += "      default:1\n";
        
        return s;
    }
    
    public void printPacket(String prefix, Packet packet)
    {
        if(this.debugLevel < 2)
        {
            return;
        }
        
        if((packet.channels & this.CHANNEL_CLIENT) == this.CHANNEL_CLIENT)
        {
            System.out.println("<2>" + this.getClass().getSimpleName() + 
                    "." + "client." + prefix + ":" + packet.getRawString());
        }
        if((packet.channels & this.CHANNEL_SERVER) == this.CHANNEL_SERVER)
        {
            System.out.println("<2>" + this.getClass().getSimpleName() + 
                    "." + "server." + prefix + ":" + packet.getRawString());
        }
        if((packet.channels & this.CHANNEL_EXTENDER) == this.CHANNEL_EXTENDER)
        {
            if(this.extenderChannel != null)
            {
                System.out.println("<2>" + this.getClass().getSimpleName() + 
                        "." + "extender." + prefix + ":" + packet.getRawString());
            }
        }
    }
    
    public Packet command(Packet cmd) throws IOException
    {
        cmd.channels = this.CHANNEL_CLIENT;
        
        this.sendPacket(cmd);
        this.recvAcknowledgment(this.CHANNEL_CLIENT);
        
        Packet res = this.recvPacket(this.CHANNEL_CLIENT);
        this.sendAcknowledgment(this.CHANNEL_CLIENT);
        
        return res;
    }
    
    public String readRegisters() throws IOException
    {
        Packet cmd = Packet.make("g");
        Packet res = this.command(cmd);
        
        return res.getString();
    }
    
    public String readMemory(long addr, long length) throws IOException
    {
        assert addr >= 0L && addr <= 0xffffffffL;
        assert length > 0 && length <= 65536;
        assert addr + length <= 0x100000000L;
        
        Packet cmd = Packet.makeReadMemory(addr, length);
        Packet res = this.command(cmd);
        
        return res.getString();
    }
    
    public String readByteString(long addr) throws IOException
    {
        return readMemory(addr, 1);
    }
    
    public int readByte(long addr) throws IOException
    {
        int ret = Integer.parseInt(this.readByteString(addr), 16);
        
        return ret;
    }
    
    public String readShortString(long addr) throws IOException
    {
        return readMemory(addr, 2);
    }
    
    public int readShort(long addr) throws IOException
    {
        int ret = Integer.parseInt(this.readShortString(addr), 16);
        ret = ((ret & 0xff) << 8) | ((ret >> 8) & 0xff);
        
        return ret;
    }
    
    public String readIntString(long addr) throws IOException
    {
        return readMemory(addr, 4);
    }
    
    public long readInt(long addr) throws IOException
    {
        long ret = Long.parseLong(this.readIntString(addr), 16);
        ret = ((ret & 0xff) << 24) | (((ret >> 8) & 0xff) << 16) | (((ret >> 16) & 0xff) << 8) | ((ret >> 24) & 0xff);
        
        return ret;
    }
    
    public String readString(long addr, int maxLength) throws IOException
    {
        String str = this.readMemory(addr, maxLength);
        int len = str.length() / 2;
        int count = 0;
        byte array[] = new byte[len];
        String s;
        int n;
        
        for(int i = 0; i < len; i++)
        {
            s = str.substring(i * 2, i * 2 + 2);
            n = Integer.parseInt(s, 16);
            if(n != 0 && n <= 127)
            {
                array[i] = (byte)n;
                count++;
            }
            else
            {
                break;
            }
        }
        
        return new String(array, 0, count);
    }
    
    public void recvAcknowledgment(int channel) throws IOException
    {
        if(this.noAckMode == false)
        {
            this.recvPacket(channel); // get Acknowledgment
        }
    }
    
    public void sendAcknowledgment(int channel) throws IOException
    {
        if(this.noAckMode == false)
        {
            Packet ack = Packet.makeSpecial(PacketType.ACK);
            ack.channels = channel;
            this.sendPacket(ack); // sent Acknowledgment
        }
    }
    
    public static void main(String[] args) {
        Node node = new Node("node");
        
        String[] remainArgs = node.parseArgs(args);
        if(remainArgs.length > 0 && remainArgs[0] != null)
        {
            for(int i = 0; i < remainArgs.length; i++)
            {
                if(remainArgs[i] == null)
                {
                    break;
                }
                
                System.out.println("unrecognized option: " + remainArgs[i]);
            }
            System.out.print(node.usage());
            return;
        }
        
        node.setLoggerLevel(Level.OFF);
        node.run();
    }
}
